/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { StringBuilder } from '../lang/stringBuilder'
import {
  EMPTY_ARRAY,
  NONEMPTY_ARRAY,
  EMPTY_OBJECT,
  DANGLING_NAME,
  NONEMPTY_OBJECT,
  EMPTY_DOCUMENT,
  NONEMPTY_DOCUMENT,
  CLOSED
} from './jsonScope'
import {
  REPLACEMENT_KEYS,
  REPLACEMENT_VALUES,
  HTML_SAFE_REPLACEMENT_KEYS,
  HTML_SAFE_REPLACEMENT_VALUES
} from './jsonReplace'

export class JsonWriter {
  private out: StringBuilder = null;
  private stack: Array<number> = new Array(32);
  private stackSize: number = 0;
  private indent: string = null;
  private lenient: boolean = false;
  private htmlSafe: boolean = true;
  private serializeNulls: boolean = false;
  private deferredName: string = null;
  private separator = ":";
  private REPLACEMENT_CHARS = {
    '"': "\\\"",
    '\\': "\\\\",
    '\t': "\\t",
    '\b': "\\b",
    '\n': "\\n",
    '\r': "\\r",
    '\f': "\\f",
    '\u0000': '\\u0000',
    '\u0001': '\\u0001',
    '\u0002': '\\u0002',
    '\u0003': '\\u0003',
    '\u0004': '\\u0004',
    '\u0005': '\\u0005',
    '\u0006': '\\u0006',
    '\u0007': '\\u0007',
    '\u000b': '\\u000b',
    '\u000e': '\\u000e',
    '\u000f': '\\u000f',
    '\u0010': '\\u0010',
    '\u0011': '\\u0011',
    '\u0012': '\\u0012',
    '\u0013': '\\u0013',
    '\u0014': '\\u0014',
    '\u0015': '\\u0015',
    '\u0016': '\\u0016',
    '\u0017': '\\u0017',
    '\u0018': '\\u0018',
    '\u0019': '\\u0019',
    '\u001a': '\\u001a',
    '\u001b': '\\u001b',
    '\u001c': '\\u001c',
    '\u001d': '\\u001d',
    '\u001e': '\\u001e',
    '\u001f': '\\u001f',
    '\u2028': '\\u2028',
    '\u2029': '\\u2029'
  };
  private HTML_SAFE_REPLACEMENT_CHARS = {
    '"': "\\\"",
    '\\': "\\\\",
    '\t': "\\t",
    '\b': "\\b",
    '\n': "\\n",
    '\r': "\\r",
    '\f': "\\f",
    '\u0000': '\\u0000',
    '\u0001': '\\u0001',
    '\u0002': '\\u0002',
    '\u0003': '\\u0003',
    '\u0004': '\\u0004',
    '\u0005': '\\u0005',
    '\u0006': '\\u0006',
    '\u0007': '\\u0007',
    '\u000b': '\\u000b',
    '\u000e': '\\u000e',
    '\u000f': '\\u000f',
    '\u0010': '\\u0010',
    '\u0011': '\\u0011',
    '\u0012': '\\u0012',
    '\u0013': '\\u0013',
    '\u0014': '\\u0014',
    '\u0015': '\\u0015',
    '\u0016': '\\u0016',
    '\u0017': '\\u0017',
    '\u0018': '\\u0018',
    '\u0019': '\\u0019',
    '\u001a': '\\u001a',
    '\u001b': '\\u001b',
    '\u001c': '\\u001c',
    '\u001d': '\\u001d',
    '\u001e': '\\u001e',
    '\u001f': '\\u001f',
    '\u2028': '\\u2028',
    '\u2029': '\\u2029',
    '<': "\\u003c",
    '>': "\\u003e",
    '&': "\\u0026",
    '=': "\\u003d",
    '\'': "\\u0027"
  };

  constructor(out: StringBuilder) {
    if (out === null || out === undefined) {
      throw new Error('out == null');
    }
    this.out = out;
    this.push(EMPTY_DOCUMENT);
  }

  public getOut(): StringBuilder {
    return this.out;
  }

  public setIndent(indent: string): void {
    if (indent.length === 0) {
      this.indent = null;
      this.separator = ":";
    } else {
      this.indent = indent;
      this.separator = ": ";
    }
  }

  public setLenient(lenient: boolean): void {
    this.lenient = lenient;
  }

  public isLenient(): boolean {
    return this.lenient;
  }

  public setHtmlSafe(htmlSafe: boolean): void {
    this.htmlSafe = htmlSafe;
  }
  public isHtmlSafe(): boolean {
    return this.htmlSafe;
  }

  public setSerializeNulls(serializeNulls: boolean) {
    this.serializeNulls = serializeNulls;
  }

  public getSerializeNulls(): boolean {
    return this.serializeNulls;
  }

  public beginArray(): JsonWriter {
    this.writeDeferredName();
    return this.open(EMPTY_ARRAY, '[');
  }

  public endArray(): JsonWriter {
    return this._close(EMPTY_ARRAY, NONEMPTY_ARRAY, ']');
  }

  public beginObject(): JsonWriter {
    this.writeDeferredName();
    return this.open(EMPTY_OBJECT, '{');
  }

  public endObject(): JsonWriter {
    return this._close(EMPTY_OBJECT, NONEMPTY_OBJECT, '}');
  }

  public name(_name: string): JsonWriter {
    if (_name === null || _name === undefined) {
      throw new Error('name == null');
    }
    if (this.deferredName != null) {
      throw new Error('JsonWriter is closed.');
    }
    this.deferredName = _name;
    return this;
  }

  public value(_value: string | boolean | number): JsonWriter {
    if (_value === null || _value === undefined) {
      return this.nullValue();
    }
    this.writeDeferredName();
    this.beforeValue();
    if (typeof _value === 'string') {
      this.string(_value);
    } else if (typeof _value === 'boolean') {
      this.out.append(_value ? 'true' : 'false');
    } else if (typeof _value === 'number') {
      this.out.append(_value.toString());
    }
    return this;
  }

  public jsonValue(value: string): JsonWriter {
    if (value === null || value === undefined) {
      return this.nullValue();
    }
    this.writeDeferredName();
    this.beforeValue();
    this.out.append(value);
    return this;
  }

  public nullValue(): JsonWriter {
    if (this.deferredName != null) {
      if (this.serializeNulls) {
        this.writeDeferredName();
      } else {
        this.deferredName = null;
        return this;
      }
    }
    this.beforeValue();
    this.out.append('null');
    return this;
  }

  public close(): void {
    let size = this.stackSize;
    if (size > 1 || size === 1 && this.stack[size - 1] != NONEMPTY_DOCUMENT) {
      throw new Error('Incomplete document');
    }
    this.stackSize = 0;
  }

  private open(empty: number, openBracket: string): JsonWriter {
    this.beforeValue();
    this.push(empty);
    this.out.append(openBracket);
    return this;
  }

  private _close(empty: number, nonempty: number, closeBracket: string) {
    let context = this.peek();
    if (context != nonempty && context != empty) {
      throw new Error('Nesting problem.')
    }
    if (this.deferredName != null) {
      throw new Error("Dangling name: " + this.deferredName);
    }
    this.stackSize--;
    if (context === nonempty) {
      this.newline();
    }
    this.out.append(closeBracket);
    return this;
  }

  private writeDeferredName(): void {
    if (this.deferredName != null) {
      this.beforeName();
      this.string(this.deferredName);
      this.deferredName = null;
    }
  }

  private beforeName() {
    let context: number = this.peek();
    if (context === NONEMPTY_OBJECT) {
      this.out.append(',');
    } else if (context != EMPTY_OBJECT) {
      throw new Error('"Nesting problem."');
    }
    this.newline();
    this.replaceTop(DANGLING_NAME);
  }

  private push(newTop: number): void {
    if (this.stack.length === this.stackSize) {
      this.stack.concat(new Array(this.stackSize))
    }
    this.stack[this.stackSize++] = newTop;
  }

  private peek(): number {
    if (this.stackSize === 0) {
      throw new Error('JsonWriter is closed.');
    }
    return this.stack[this.stackSize - 1];
  }

  private newline(): void {
    if (this.indent === null || this.indent === undefined) {
      return;
    }
    this.out.append('\n');
    for (let i = 1, size = this.stackSize; i < size; i++) {
      this.out.append(this.indent);
    }
  }

  private string(value: string): void {
    this.out.append('"');
    let last = 0;
    let length = value.length;
    let replacements = this.htmlSafe ? this.HTML_SAFE_REPLACEMENT_CHARS : this.REPLACEMENT_CHARS;
    for (let i = 0; i < length; i++) {
      let c = value[i];
      let replacement;
      if (replacements.hasOwnProperty(c)) {
        replacement = replacements[c];
      } else {
        continue;
      }
      if (last < i) {
        this.out.append(value.slice(last, i));
      }
      this.out.append(replacement);
      last = i + 1;
    }
    if (last < length) {
      this.out.append(value.slice(last, length));
    }
    this.out.append('"');
  }

  private replaceTop(topOfStack: number): void {
    this.stack[this.stackSize - 1] = topOfStack;
  }

  private beforeValue(): void {
    switch (this.peek()) {
      case NONEMPTY_DOCUMENT:
        if (!this.lenient) {
          throw new Error('JSON must have only one top-level value.');
        }
        break;
      case EMPTY_DOCUMENT:
        this.replaceTop(NONEMPTY_DOCUMENT);
        break;
      case EMPTY_ARRAY:
        this.replaceTop(NONEMPTY_ARRAY);
        this.newline();
        break;
      case NONEMPTY_ARRAY:
        this.out.append(',');
        this.newline();
        break;
      case DANGLING_NAME:
        this.out.append(this.separator);
        this.replaceTop(NONEMPTY_OBJECT);
        break;
      default:
        throw new Error('Nesting problem.')
        break;
    }
  }
}